首先讓我們回顧一下可愛的StatelessWidget
:
class Foo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
嗯沒什麼特別的,一個Widget
subclass,複寫一個build
函數來建立子樹,非常簡單直接。
接著再來看看今天的主角StatefulWidget
:
class Bar extends StatefulWidget {
@override
_BarState createState() => _BarState();
}
class _BarState extends State<Bar> {
@override
Widget build(BuildContext context) {
return Container();
}
}
哇事情一下就複雜起來了!Bar
有個createState()
去建立_BarState()
,而_BarState
則是繼承了State<Bar>
。到這裡勉強還說得通,但是_BarState
裡面竟然有個跟StatelessWidget
一樣的build
函數,這是什麼巫術?為什麼build
不是放在StatefulWidget
裡面?這樣寫起來既複雜又不對稱,實在是讓人很不舒服。而且State
類別聽起來就是單純存放資料的,現在竟然還要負責建立子樹,這不是很奇怪嗎?
這可以說是我最初在學Flutter時產生的第一個疑問,相信很多人第一眼看到StatefulWidget
時心理也曾經出現過這樣的想法,今天就讓我們試著來釐清這個問題。
StatefulWidget
時的彈性解釋這點之前,我們先來瞭解一下AnimatedWidget
怎麼運作,這是它的部份原始碼:
abstract class AnimatedWidget extends StatefulWidget {
/// Override this method to build widgets that depend on the state of the
/// listenable (e.g., the current value of the animation).
@protected
Widget build(BuildContext context);
}
class _AnimatedState extends State<AnimatedWidget> {
@override
Widget build(BuildContext context) => widget.build(context);
}
你可以看到,AnimatedWidget
繼承了StatefulWidget
,並宣告一個abstract build
(注意這不是從StatefulWidget
繼承來的,而是AnimatedWidget
自己宣告的)。接著_AnimatedState
的build
,透過widget
回去呼叫那個abstract build
。為什麼要這麼做?讓我們來看看AnimatedWidget
的使用方式:
class MyRotatingWidget extends AnimatedWidget {
const MyRotatingWidget({
Key key,
AnimationController controller,
}) : super(key: key, listenable: controller,);
Animation<double> get _progress => listenable;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: _progress.value,
child: FlutterLogo(),
);
}
}
動畫一定是有狀態_progress
的,而狀態就須要StatefulWidget
來管理,但如果我們唯一的狀態只有_progress
,其它部份都是...stateless呢?我們還需要自己分別繼承StatefulWidget
和State
嗎?這就是AnimatedWidget
幫你處理的事情,它幫你繼承去StatefulWidget
和State
並管理_progress
。然後它宣告一個build
函數給你覆寫,就好像你是在繼承StatelessWidget
一樣。你不須要自己建立和管理狀態,也不須要知道AnimatedWidget
背後其實有著StatefulWidget
這樣的實作細節。
好,最後讓我們回到一開始的問題,如果我們把build
搬回StatefulWidget
會怎樣呢?
abstract class StatefulWidget extends Widget {
Widget build(BuildContext context, State state);
}
// option 1: extra build function
// result : confusing multiple build function
class AnimatedWidget extends StatefulWidget {
@protected
Widget build(BuildContext context);
}
// option 2: expose build function from StatefulWidget as-is
// result : unnecessarily expose [state], which should be an implementation detail
class AnimatedWidget extends StatefulWidget {
// do nothing
}
這時候StatefulWidget
的build
就必須接收一個State
物件,畢竟你終究須要存放在裡面的狀態變數來建立子樹。而AnimatedWidget
繼承StatefulWidget
時就同時繼承了build(context, state)
,也就進一步把state
這個實作細節暴露出去了。
AnimatedWidget
只是其中一個例子,基本上任何時候,當我們想建立一個custom abstract widget來讓其它class繼承,幫child class管理狀態並隱藏起來時,如果有build(context, state)
存在在StatefulWidget
裡面,就會導致實作細節的暴露了。
Closure
中隱含的this
造成的bugclass MyButton extends StatefulWidget {
MyButton(this.color);
final Color color;
@override
Widget build(BuildContext context, State state) => FlatButton(
onPressed: () { print('color: $color'); },
);
}
假設今天有個MyButton
繼承了我們新的StatefulWidget
,其中有個屬性color
。MyButton
第一次被parent建立時被傳入Colors.blue
,這時print
裡的$color
會是blue
沒錯。但如果parent決定重新建立MyButton
並傳入Colors.green
,這時候print
的$color
卻依然會是blue
。原因在於雖然MyButton
是新的實例,但是FlatButton
卻不會被重建,因為它沒有任何改變。也就是說這時onPressed
被賦予的Closure () { print('color: $color'); }
也一樣是舊的Closure,而這裡面隱含取用的this
也是舊的this
,也就是舊的MyButton
。
class MyButtonState extends State<MyButton> {
@override
Widget build(BuildContext context) => FlatButton(
onPressed: () { print('color: $widget.color'); },
);
}
相較之下,如果我們的build是在State裡面,因為我們是透過widget.color
來取得color
,而這裡的widget
參照是會在StatefulWidget重新建立時被更新的,也就避免了上述的bug。
以上兩點是官方針對這個問題給出的回應,聽起來合理但好像又不是那麼有說服力。畢竟Stateless/StatefulWidget是Flutter最基本也最常被使用的元件,就為了這兩個感覺不怎麼嚴重的問題而讓整個設計複雜化,感覺是不是有點太小題大作了?說到底這一切問題的根源還是在於,一個被命名為State
的類別負擔了太多不該是State
的責任。如果一開始把它叫做ViewModel
或Controller
啥的,或許大家就不會那麼混亂了?